package org.infodavid.common.reflect;

import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The Class PackageUtil.
 */
@SuppressWarnings(value = {
  "rawtypes"
})
public final class PackageUtil {
  /** The Constant log. */
  private static final Log LOGGER = LogFactory.getLog(PackageUtil.class);// NOPMD

  /** The singleton. */
  private static WeakReference<PackageUtil> singleton = null;

  /**
   * returns the singleton.
   * @return the singleton
   */
  public static PackageUtil getInstance() {
    if (singleton == null || singleton.get() == null) {
      singleton = new WeakReference<PackageUtil>(new PackageUtil());
    }

    return singleton.get();
  }

  /**
   * Instantiates a new reflect util.
   */
  private PackageUtil() {
    super();
  }

  /**
   * Gets the classes.
   * @param pack the pack
   * @param filter the filter
   * @param recursive the recursive
   * @return the classes
   * @throws ClassNotFoundException the class not found exception
   * @throws IOException Signals that an I/O exception has occurred.
   * @throws URISyntaxException the uRI syntax exception
   */
  public Set<Class> getClasses(final Package pack, final IClassNameFilter filter,
      final boolean recursive) throws ClassNotFoundException, IOException, URISyntaxException {
    return getClasses(pack.getName(), filter, recursive);
  }

  /**
   * Scans all classes accessible from the context class loader which belong to the given package
   * and sub-packages.
   * @param packageName the package name
   * @param filter the filter
   * @param recursive the recursive
   * @return the classes
   * @throws ClassNotFoundException the class not found exception
   * @throws IOException Signals that an I/O exception has occurred.
   * @throws URISyntaxException the uRI syntax exception
   */
  public Set<Class> getClasses(final String packageName, final IClassNameFilter filter,
      final boolean recursive) throws ClassNotFoundException, IOException, URISyntaxException {
    final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

    assert classLoader != null;

    final String path = packageName.replace('.', '/');
    final Enumeration<URL> resources = classLoader.getResources(path);
    final List<File> dirs = new ArrayList<File>();
    URL resourceUrl;
    URI resourceUri;

    while (resources.hasMoreElements()) {
      resourceUrl = resources.nextElement();
      resourceUri = resourceUrl.toURI();

      if (!resourceUri.isOpaque()) {
        dirs.add(new File(resourceUri));
      }
    }

    final Set<Class> classes = new HashSet<Class>();

    for (final File directory : dirs) {
      findClasses(directory, packageName, recursive, filter, classes);
    }

    return classes;
  }

  /**
   * Recursive method used to find all classes in a given directory and sub-directories.
   * @param directory The base directory
   * @param packageName The package name for classes found inside the base directory
   * @param recursive the recursive
   * @param filter the filter
   * @param classes the classes
   * @throws ClassNotFoundException the class not found exception
   */
  private static void findClasses(final File directory, final String packageName,
      final boolean recursive, final IClassNameFilter filter, final Set<Class> classes)
      throws ClassNotFoundException {
    if (!directory.exists()) {
      return;
    }

    final File[] files = directory.listFiles();

    for (final File file : files) {
      final String fileName = file.getName();

      if (file.isDirectory() && recursive) {
        assert !fileName.contains(".");

        findClasses(file, packageName + "." + fileName, recursive, filter, classes);
      }
      else if (fileName.endsWith(".class") && !fileName.contains("$")) {
        final String className = packageName + '.' + fileName.substring(0, fileName.length() - 6);

        if (filter == null || filter.accept(className)) {
          Class _class = null;

          try {
            _class = Class.forName(className);
          }
          catch (final ExceptionInInitializerError e) {
            // happen, for example, in classes, which depend on
            // Spring to inject some beans, and which fail,
            // if dependency is not fulfilled
            _class =
                Class.forName(className, false, Thread.currentThread().getContextClassLoader());
          }
          catch (final Throwable e) {
            LOGGER.debug("An error occurs while loading class: " + className);
          }

          if (_class != null) {
            classes.add(_class);
          }
        }
      }
    }
  }
}
